Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit 3b72902317ebff1912be4116801331d5dd056068


Parents : 5008189
Author : Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-05-09T21:00:38-05:00

test(nomadnet): add tests for query-param file downloads and docs fixes

- Add backend tests for NomadnetFileDownloader data parameter

- Add WS order test verifying data reaches the downloader

- Add docs_manager tests for index.html generation and directory rejection

- Extend security fuzzing to cover data field in file downloads

- Add frontend tests for parseNomadnetworkUrl with query strings

- Add frontend tests for downloadNomadNetFile data payload

- Fix meshchat.py NomadnetFileDownloader kwarg name regression

Changes
Diff

diff --git a/meshchatx/meshchat.py b/meshchatx/meshchat.py
index 3c74e640..5da111f0 100644
--- a/meshchatx/meshchat.py
+++ b/meshchatx/meshchat.py
@@ -14110,10 +14110,10 @@ class ReticulumMeshChat:
downloader = NomadnetFileDownloader(
destination_hash,
file_path,
+ on_file_download_success,
+ on_file_download_failure,
+ on_file_download_progress,
data=request_data,
- on_file_download_success=on_file_download_success,
- on_file_download_failure=on_file_download_failure,
- on_file_download_progress=on_file_download_progress,
on_phase=on_file_download_phase,
reticulum=getattr(self, "reticulum", None),
)

diff --git a/tests/backend/test_docs_manager.py b/tests/backend/test_docs_manager.py
index 59472471..0a418a34 100644
--- a/tests/backend/test_docs_manager.py
+++ b/tests/backend/test_docs_manager.py
@@ -331,3 +331,38 @@ def test_extract_docs_malformed_zip(docs_manager, temp_dirs):
finally:
if os.path.exists(zip_path):
os.remove(zip_path)
+
+
+def test_populate_meshchatx_docs_generates_index_html(tmp_path):
+ public_dir = tmp_path / "public"
+ public_dir.mkdir()
+ docs_dir = tmp_path / "docs"
+ docs_dir.mkdir()
+ (docs_dir / "README.md").write_text("# Hello\nWorld")
+ (docs_dir / "FAQ.md").write_text("# FAQ\nQ&A")
+
+ config = MagicMock()
+ dm = DocsManager(config, str(public_dir), project_root=str(tmp_path))
+ dm.populate_meshchatx_docs()
+
+ index_path = os.path.join(dm.meshchatx_docs_dir, "index.html")
+ assert os.path.exists(index_path)
+ content = open(index_path, encoding="utf-8").read()
+ assert "MeshChatX Documentation" in content
+ assert "README.html" in content
+ assert "FAQ.html" in content
+
+
+def test_get_doc_content_rejects_directory_path(tmp_path):
+ public_dir = tmp_path / "public"
+ public_dir.mkdir()
+ config = MagicMock()
+ dm = DocsManager(config, str(public_dir))
+
+ # Ensure meshchatx_docs_dir exists as a directory
+ os.makedirs(dm.meshchatx_docs_dir, exist_ok=True)
+
+ # Passing "." should resolve to the directory itself, not a file
+ assert dm.get_doc_content(".") is None
+ assert dm.get_doc_content("..") is None
+ assert dm.get_doc_content("") is None

diff --git a/tests/backend/test_nomadnet_download_ws_order.py b/tests/backend/test_nomadnet_download_ws_order.py
index 1ca453f6..69285bc6 100644
--- a/tests/backend/test_nomadnet_download_ws_order.py
+++ b/tests/backend/test_nomadnet_download_ws_order.py
@@ -89,3 +89,53 @@ async def test_nomadnet_file_download_started_before_download_async_scheduled(
assert events[0][1]["type"] == "nomadnet.file.download"
assert events[0][1]["nomadnet_file_download"]["status"] == "started"
assert events[1][0] == "run_async"
+
+
+@pytest.mark.asyncio
+async def test_nomadnet_file_download_with_data_passed_to_downloader(
+ mock_app, monkeypatch
+):
+ """Query-param data from the WS payload must reach NomadnetFileDownloader."""
+ mock_app._try_serve_local_page_node_file = MagicMock(return_value=None)
+
+ from meshchatx.src.backend import nomadnet_downloader
+
+ captured = {}
+ orig_init = nomadnet_downloader.NomadnetFileDownloader.__init__
+
+ def capturing_init(self, *args, **kwargs):
+ captured["args"] = args
+ captured["kwargs"] = kwargs
+ # Don't call real init to avoid RNS side-effects
+ self.is_cancelled = False
+ self.destination_hash = args[0]
+ self.path = args[1]
+ self.data = kwargs.get("data")
+
+ monkeypatch.setattr(
+ nomadnet_downloader.NomadnetFileDownloader, "__init__", capturing_init
+ )
+
+ mock_ws = MagicMock()
+ mock_ws.send_str = AsyncMock()
+
+ dh = "c" * 32
+ await mock_app.on_websocket_data_received(
+ mock_ws,
+ {
+ "type": "nomadnet.file.download",
+ "nomadnet_file_download": {
+ "destination_hash": dh,
+ "file_path": "/files/report.pdf",
+ "data": "version=2&format=raw",
+ },
+ },
+ )
+
+ assert captured["kwargs"].get("data") == "version=2&format=raw"
+ assert captured["args"][0] == bytes.fromhex(dh)
+ assert captured["args"][1] == "/files/report.pdf"
+
+ monkeypatch.setattr(
+ nomadnet_downloader.NomadnetFileDownloader, "__init__", orig_init
+ )

diff --git a/tests/backend/test_nomadnet_downloader_boost.py b/tests/backend/test_nomadnet_downloader_boost.py
index 662feb9b..fbdfefc7 100644
--- a/tests/backend/test_nomadnet_downloader_boost.py
+++ b/tests/backend/test_nomadnet_downloader_boost.py
@@ -188,6 +188,37 @@ def test_file_downloader_list_response_short_list_no_crash():
on_fail.assert_called_once_with("unsupported_response")
+def test_file_downloader_stores_data_parameter():
+ on_ok = MagicMock()
+ on_fail = MagicMock()
+ on_progress = MagicMock()
+ fd = NomadnetFileDownloader(
+ b"ab" * 8,
+ "/file/data.bin",
+ on_ok,
+ on_fail,
+ on_progress,
+ data="query=value&other=123",
+ )
+ assert fd.data == "query=value&other=123"
+
+
+def test_file_downloader_passes_data_to_parent():
+ on_ok = MagicMock()
+ on_fail = MagicMock()
+ on_progress = MagicMock()
+ fd = NomadnetFileDownloader(
+ b"ab" * 8,
+ "/file/data.bin",
+ on_ok,
+ on_fail,
+ on_progress,
+ data="foo=bar",
+ )
+ # NomadnetDownloader stores data as the 3rd positional arg
+ assert fd.data == "foo=bar"
+
+
def test_cache_lock_serializes_mutations():
mock_link = MagicMock()
mock_link.status = RNS.Link.ACTIVE

diff --git a/tests/backend/test_security_fuzzing.py b/tests/backend/test_security_fuzzing.py
index aac2fb2f..ea1f406f 100644
--- a/tests/backend/test_security_fuzzing.py
+++ b/tests/backend/test_security_fuzzing.py
@@ -1430,9 +1430,10 @@ def test_nomadnet_page_archive_add_fuzzing(
@given(
destination_hash=st.text(min_size=0, max_size=200),
file_path=st.text(min_size=0, max_size=2000),
+ data=st.one_of(st.none(), st.text(min_size=0, max_size=2000)),
)
-def test_nomadnet_file_download_fuzzing(mock_app, destination_hash, file_path):
- """Fuzz nomadnet.file.download WebSocket handler (path traversal, malformed hash)."""
+def test_nomadnet_file_download_fuzzing(mock_app, destination_hash, file_path, data):
+ """Fuzz nomadnet.file.download WebSocket handler (path traversal, malformed hash, data)."""
import asyncio
loop = asyncio.new_event_loop()
@@ -1443,6 +1444,7 @@ def test_nomadnet_file_download_fuzzing(mock_app, destination_hash, file_path):
"nomadnet_file_download": {
"destination_hash": destination_hash,
"file_path": file_path,
+ "data": data,
},
}
loop.run_until_complete(

diff --git a/tests/frontend/NomadNetworkPage.test.js b/tests/frontend/NomadNetworkPage.test.js
index 0a23bcef..b1a26f16 100644
--- a/tests/frontend/NomadNetworkPage.test.js
+++ b/tests/frontend/NomadNetworkPage.test.js
@@ -404,4 +404,117 @@ describe("NomadNetworkPage.vue", () => {
expect(wrapper.vm.hasPageLoadFailed).toBe(false);
});
});
+
+ describe("parseNomadnetworkUrl", () => {
+ it("parses absolute URL with query string", () => {
+ const wrapper = mountNomadNetworkPage();
+ const dest = "a".repeat(32);
+ const result = wrapper.vm.parseNomadnetworkUrl(`${dest}:/file/report.pdf?version=2&format=raw`);
+ expect(result).toEqual({
+ destination_hash: dest,
+ path: "/file/report.pdf",
+ query: "version=2&format=raw",
+ });
+ });
+
+ it("parses relative URL with query string", () => {
+ const wrapper = mountNomadNetworkPage();
+ wrapper.vm.defaultNodePagePath = "/page/index.mu";
+ const result = wrapper.vm.parseNomadnetworkUrl(":/file/data.bin?key=val");
+ expect(result).toEqual({
+ destination_hash: null,
+ path: "/file/data.bin",
+ query: "key=val",
+ });
+ });
+
+ it("parses node-only URL without query", () => {
+ const wrapper = mountNomadNetworkPage();
+ const dest = "b".repeat(32);
+ const result = wrapper.vm.parseNomadnetworkUrl(dest);
+ expect(result).toEqual({
+ destination_hash: dest,
+ path: wrapper.vm.defaultNodePagePath,
+ query: null,
+ });
+ });
+
+ it("parses absolute URL without query", () => {
+ const wrapper = mountNomadNetworkPage();
+ const dest = "c".repeat(32);
+ const result = wrapper.vm.parseNomadnetworkUrl(`${dest}:/page/index.mu`);
+ expect(result).toEqual({
+ destination_hash: dest,
+ path: "/page/index.mu",
+ query: null,
+ });
+ });
+
+ it("returns null for unsupported URL", () => {
+ const wrapper = mountNomadNetworkPage();
+ expect(wrapper.vm.parseNomadnetworkUrl("not-a-url")).toBeNull();
+ });
+
+ it("handles empty query string after ?", () => {
+ const wrapper = mountNomadNetworkPage();
+ const dest = "d".repeat(32);
+ const result = wrapper.vm.parseNomadnetworkUrl(`${dest}:/file/x.txt?`);
+ expect(result.path).toBe("/file/x.txt");
+ expect(result.query).toBe("");
+ });
+ });
+
+ describe("downloadNomadNetFile", () => {
+ let WebSocketConnection;
+
+ beforeEach(async () => {
+ // Re-import to get the mocked module
+ WebSocketConnection = (await import("@/js/WebSocketConnection")).default;
+ WebSocketConnection.send.mockClear();
+ });
+
+ it("includes data in websocket payload when provided", () => {
+ const wrapper = mountNomadNetworkPage();
+ wrapper.vm.downloadNomadNetFile(
+ "a".repeat(32),
+ "/file/data.bin",
+ "version=2&format=raw",
+ vi.fn(),
+ vi.fn(),
+ vi.fn(),
+ );
+ expect(WebSocketConnection.send).toHaveBeenCalledOnce();
+ const payload = JSON.parse(WebSocketConnection.send.mock.calls[0][0]);
+ expect(payload.type).toBe("nomadnet.file.download");
+ expect(payload.nomadnet_file_download.data).toBe("version=2&format=raw");
+ });
+
+ it("omits data field when data is null", () => {
+ const wrapper = mountNomadNetworkPage();
+ wrapper.vm.downloadNomadNetFile(
+ "b".repeat(32),
+ "/file/data.bin",
+ null,
+ vi.fn(),
+ vi.fn(),
+ vi.fn(),
+ );
+ const payload = JSON.parse(WebSocketConnection.send.mock.calls[0][0]);
+ expect(payload.nomadnet_file_download).not.toHaveProperty("data");
+ });
+
+ it("omits data field when data is undefined", () => {
+ const wrapper = mountNomadNetworkPage();
+ wrapper.vm.downloadNomadNetFile(
+ "c".repeat(32),
+ "/file/data.bin",
+ undefined,
+ vi.fn(),
+ vi.fn(),
+ vi.fn(),
+ );
+ const payload = JSON.parse(WebSocketConnection.send.mock.calls[0][0]);
+ expect(payload.nomadnet_file_download).not.toHaveProperty("data");
+ });
+ });
});


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────